iT邦幫忙

2025 iThome 鐵人賽

DAY 5
1
Rust

大家一起跟Rust當好朋友吧!系列 第 5

Day 5: 參考 (References) 與借用 (Borrowing):不轉移所有權的資料傳遞

  • 分享至 

  • xImage
  •  

嗨嗨!大家好!歡迎來到 Rust 三十天挑戰的第五天!

昨天我們學習了所有權這個核心概念,但你可能已經發現了一個問題:如果每次把變數傳給函式就會轉移所有權,那程式碼會變得非常難寫和不實用。想像一下,如果你想計算一個字串的長度,卻因此失去了這個字串的使用權,這根本不合理對吧?

幸好,Rust 提供了一個優雅的解決方案:參考 (References) 與借用 (Borrowing)

今天我們要學習如何在不轉移所有權的情況下,讓函式能夠使用資料。這就像是把你的書「借」給朋友看,而不是直接「送」給他一樣 —— 你依然是書的主人,朋友只是暫時使用而已。

什麼是參考?

參考就像是變數的「別名」或「指向」,它讓我們可以使用某個值,但不擁有它。在 Rust 中,我們使用 & 符號來建立參考:

fn main() {
    let s1 = String::from("hello");
    let len = calculate_length(&s1);  // 傳遞 s1 的參考
    
    println!("'{}' 的長度是 {}", s1, len);  // s1 仍然可以使用!
}

fn calculate_length(s: &String) -> usize {  // s 是 String 的參考
    s.len()
}  // s 離開作用域,但因為它沒有擁有資料,所以不會釋放記憶體

在這個例子中,&s1 建立了一個指向 s1 的參考,而函式參數 s: &String 接收這個參考。重要的是,s1 的所有權沒有轉移,所以我們之後還能繼續使用它!

借用的規則

我們將使用參考稱為「借用」(borrowing),因為我們只是借用值而不擁有它。但借用有一些重要的規則:

規則 1:參考必須總是有效的

fn main() {
    let reference_to_nothing = dangle();  // 這會編譯錯誤!
}

fn dangle() -> &String {  // 試圖回傳一個參考
    let s = String::from("hello");
    &s  // 回傳 s 的參考,但 s 即將被釋放!
}  // s 離開作用域並被釋放,所以參考指向了無效的記憶體

Rust 編譯器會阻止這種「懸置參考」(dangling references) 的產生。

規則 2:借用檢查器的黃金規則

在任何給定時間,你可以擁有:

  • 一個可變參考,或者
  • 任意數量的不可變參考

但不能同時擁有兩者!

讓我們看看為什麼需要這個規則:

fn main() {
    let mut s = String::from("hello");
    
    let r1 = &s;      // 沒問題 - 不可變參考
    let r2 = &s;      // 沒問題 - 可以有多個不可變參考
    println!("{} and {}", r1, r2);  // r1 和 r2 在這裡結束使用
    
    let r3 = &mut s;  // 沒問題 - 可變參考
    println!("{}", r3);
}

但如果同時存在可變和不可變參考就會出錯:

fn main() {
    let mut s = String::from("hello");
    
    let r1 = &s;      // 不可變參考
    let r2 = &mut s;  // 編譯錯誤!不能在不可變參考存在時建立可變參考
    
    println!("{} {}", r1, r2);
}

可變參考 (Mutable References)

如果我們想要透過參考來修改值,就需要使用可變參考:

fn main() {
    let mut s = String::from("hello");
    
    change(&mut s);  // 傳遞可變參考
    
    println!("{}", s);  // 輸出:hello, world
}

fn change(some_string: &mut String) {
    some_string.push_str(", world");
}

注意幾個重要的點:

  1. 原始變數必須是 mut
  2. 建立參考時使用 &mut
  3. 函式參數型別是 &mut String

可變參考的限制

同一時間只能有一個可變參考:

fn main() {
    let mut s = String::from("hello");
    
    let r1 = &mut s;
    let r2 = &mut s;  // 編譯錯誤!
    
    println!("{}, {}", r1, r2);
}

這個限制防止了「資料競爭」(data races),確保在修改資料時不會有其他代碼同時讀取或修改相同的記憶體。

參考的生命週期

參考的生命週期必須在被參考值的生命週期內:

fn main() {
    let r;                    // 宣告 r,但還沒初始化
    
    {
        let x = 5;
        r = &x;               // 錯誤!x 的生命週期太短
    }                         // x 在這裡被釋放
    
    println!("r: {}", r);     // r 參考了已被釋放的記憶體
}

正確的做法:

fn main() {
    let x = 5;                // x 進入作用域
    let r = &x;               // r 參考 x
    
    println!("r: {}", r);     // 沒問題,x 仍然有效
}                             // x 和 r 都離開作用域

字串切片 (String Slices)

除了參考整個 String,我們還可以參考字串的一部分,這叫做「字串切片」:

fn main() {
    let s = String::from("hello world");
    
    let hello = &s[0..5];    // 或 &s[..5]
    let world = &s[6..11];   // 或 &s[6..]
    let whole = &s[..];      // 整個字串的切片
    
    println!("hello: {}", hello);  // hello
    println!("world: {}", world);  // world
    println!("whole: {}", whole);  // hello world
}

字串切片的型別是 &str,它是一個不可變參考。

字串字面值就是切片

fn main() {
    let s = "Hello, world!";  // s 的型別是 &str
    
    // 字串字面值是程式二進位檔中特定位置的切片
    // 這也是為什麼字串字面值是不可變的
}

更好的函式設計

使用字串切片可以讓函式更靈活:

fn first_word(s: &String) -> &str {  // 只能接受 String 的參考
    let bytes = s.as_bytes();
    
    for (i, &item) in bytes.iter().enumerate() {
        if item == b' ' {
            return &s[0..i];
        }
    }
    
    &s[..]
}

fn first_word_better(s: &str) -> &str {  // 可以接受 &String 和 &str
    let bytes = s.as_bytes();
    
    for (i, &item) in bytes.iter().enumerate() {
        if item == b' ' {
            return &s[0..i];
        }
    }
    
    s
}

fn main() {
    let my_string = String::from("hello world");
    let word1 = first_word(&my_string);        // 只能這樣
    let word2 = first_word_better(&my_string); // 可以這樣
    let word3 = first_word_better("hello");    // 也可以這樣
    
    println!("{}, {}, {}", word1, word2, word3);
}

其他型別的切片

切片不僅適用於字串,也適用於其他集合:

fn main() {
    let a = [1, 2, 3, 4, 5];
    let slice = &a[1..3];  // 型別是 &[i32]
    
    println!("切片:{:?}", slice);  // [2, 3]
    
    // 將切片傳給函式
    print_slice(slice);
    print_slice(&a[..]);  // 傳遞整個陣列的切片
}

fn print_slice(slice: &[i32]) {
    for item in slice {
        println!("值:{}", item);
    }
}

常見的借用錯誤與解決方案

錯誤 1:同時使用可變和不可變參考

// ❌ 錯誤的寫法
fn wrong_way() {
    let mut s = String::from("hello");
    let r1 = &s;
    let r2 = &mut s;  // 錯誤!
    println!("{} {}", r1, r2);
}

// ✅ 正確的寫法
fn right_way() {
    let mut s = String::from("hello");
    let r1 = &s;
    println!("{}", r1);  // r1 的最後使用
    
    let r2 = &mut s;     // 沒問題,r1 已經不再使用
    println!("{}", r2);
}

錯誤 2:試圖回傳局部變數的參考

// ❌ 錯誤的寫法
fn wrong_return() -> &String {
    let s = String::from("hello");
    &s  // 錯誤!s 即將被釋放
}

// ✅ 正確的寫法
fn right_return() -> String {
    let s = String::from("hello");
    s  // 回傳所有權
}

// ✅ 或者接受參考參數
fn process_and_return(input: &str) -> &str {
    // 處理 input 並回傳它的一部分
    &input[0..1]
}

今天的收穫

今天我們學會了參考與借用的核心概念:

參考的基本概念

  • 使用 & 建立參考,&mut 建立可變參考
  • 參考讓我們使用值而不擁有它
  • 參考必須總是指向有效的資料

借用的黃金規則

  • 同一時間只能有一個可變參考,或任意數量的不可變參考
  • 不能同時擁有可變和不可變參考

實用技巧

  • 函式參數使用參考避免不必要的所有權轉移
  • 字串切片 &str&String 更靈活
  • 陣列切片 &[T] 可以處理陣列的一部分

為什麼這樣設計?

  • 防止資料競爭
  • 避免懸置指標
  • 零成本抽象(參考在執行時沒有額外開銷)
  • 編譯時保證記憶體安全

今天的小挑戰

寫一個程式實作以下功能:

  1. 建立一個函式 get_length(s: &String) -> usize,回傳字串的長度
  2. 建立一個函式 capitalize_first_letter(s: &mut String),將字串的第一個字母改為大寫
  3. 建立一個函式 count_words(text: &str) -> usize,計算字串中的單字數量
  4. main 函式中測試這些函式
fn main() {
    // 測試 get_length
    let text = String::from("Hello, world!");
    let length = get_length(&text);
    println!("字串長度:{}", length);
    
    // 測試 capitalize_first_letter
    let mut greeting = String::from("hello, rust!");
    println!("修改前:{}", greeting);
    capitalize_first_letter(&mut greeting);
    println!("修改後:{}", greeting);
    
    // 測試 count_words
    let sentence = "Rust is awesome and powerful";
    let word_count = count_words(sentence);
    println!("單字數量:{}", word_count);
}

fn get_length(s: &String) -> usize {
    // 你來實作!
}

fn capitalize_first_letter(s: &mut String) {
    // 你來實作!
}

fn count_words(text: &str) -> usize {
    // 你來實作!
}

學習重點

  • 練習使用不可變參考 (&String, &str)
  • 練習使用可變參考 (&mut String)
  • 理解參考如何避免所有權轉移
  • 體會參考的零成本特性

注意這個函式回傳 String 而不是 &str,因為我們需要建立一個新的字串。在後續學習生命週期的概念之後,我們會學到如何更優雅地處理這類問題。

明天我們將學習結構體與列舉,這些自訂型別將讓我們能夠建立更複雜、更有意義的資料結構。我們也會看到參考在結構體中的應用!

參考與借用是 Rust 中非常重要的概念,它們讓我們能夠寫出高效且安全的程式碼。一旦掌握了這些概念,你會發現 Rust 程式設計的美妙之處!

那麼!我們明天見!


上一篇
Day 4: 所有權 (Ownership):Rust 最核心的概念!
下一篇
Day 6: 結構 (Structs) 與列舉 (Enums):打造自己的資料型別
系列文
大家一起跟Rust當好朋友吧!20
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言